iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Modern Web

Go,一起成為全端吧!—— 給前端工程師的 Golang 後端學習筆記系列 第 4

Day4 - Go 基本語法介紹-2:陣列、函式、結構與指標

  • 分享至 

  • xImage
  •  

上一章我們有提到了 Go 的變數宣告以及迴圈的用法,今天要來繼續介紹 Go 的基本語法:陣列、函式、struct以及指標。
/images/emoticon/emoticon08.gif


Go 的基本語法-2

這個部分一樣會用簡單的範例讓大家理解用法和需要注意的地方。
終於來到陣列系列了!我自己覺得這部分的宣告是蠻不一樣的~ 不知道大家是不是也這樣覺得 👀
好!那我們先從陣列開始~

6. array & slice

Go 的陣列宣告需要一開始就指定長度,而且宣告後就不能再改變。以下是陣列宣告的 3 步驟:

  1. length:決定陣列長度(可直接給一個數字或用三個點 ”” 讓它自己計算)
  2. datatype:決定資料型態
  3. values:要放進陣列裡的初始值
// 宣告方式
var array_name [length]datatype{values}
array_name := [length]datatype{values}
    
// 範例
var numbers [5]int{1, 2, 3, 4, 5}
names := [3]string{"Apple", "Banana", "Candy"}
    
// 自己計算長度
var numbers [...]int{1, 2, 3, 4, 5}              // 自動根據內容將長度設為 5
names := [...]string{"Apple", "Banana", "Candy"} // 自動根據內容將長度設為 3

再來如果要取得陣列長度的話,可以使用 len() ,例如:len(numbers) ,就會取得 numbers 陣列的長度 5。

一開始的時候有提到不能更改陣列的長度,但陣列的內容是可以修改的!
從下面的範例可以看到我們用 Peach 取代了原本第 0 個位置的 Apple。

// 範例
names := [...]string{"Apple", "Banana", "Candy"}
names[0] = "Peach"
    
fmt.Println(names[0]) // 輸出:Peach

再來是 slice 的部分。slice 的長度是可以改變的!主要由以下 3 種結構組合成的:

  1. 指標(Pointer):指向底層 array 的某個元素位置
  2. 長度(Length):slice 目前包含的元素數量,可透過 len() 函數取得。
  3. 容量(Capacity):slice 的起始位置到底層陣列結束位置的元素總數,可透過 cap() 函數取得。

首先,先來看 slice 的建立方式:

// 範例
    
// 空 slice(nil slice)
var s []int             // 長度=0, 容量=0
    
// 1. 包含預設元素
nums := []int{1, 2, 3}  // 長度=3, 容量=3
fmt.Println(nums)       // 輸出:[1 2 3]

// 2. 使用 make()
nums1 := make([]int, 3, 5)  // 長度=3, 容量=5
fmt.Println(nums1)          // 輸出:[0 0 0]
    
// 3. 從 array 分割
arr := [5]int{10, 20, 30, 40, 50}
nums2 := arr[1:4]          // 包含 arr[1], arr[2], arr[3]
fmt.Println(nums2)         // 輸出:[20 30 40]
fmt.Println(len(nums2))    // 長度=3
fmt.Println(cap(nums2))    // 容量=4 (因為從 arr[1] 到 arr[4] 總共有 4 個空間)

再來是更改元素以及新增元素:

// 範例
    
// 更改元素
arr := [5]int{1, 2, 3, 4, 5}   // 建立一長度 5 的陣列
num4 := arr[1:4]               // 建立一 slice num4,包含 arr[1], arr[2], arr[3]
num4[0] = 99                   // 改變 num4 的元素 → [99 3 4]
fmt.Println(arr)               // 輸出:[1 99 3 4 5]  → 底層 array 也被改變
    
// 新增元素
nums5 := []int{1, 2, 3}        // 建立一 slice nums5(len=3, cap=3)
nums5 = append(nums5, 4, 5)    // 新增 4, 5
fmt.Println(nums5)             // 輸出:[1 2 3 4 5]

👉 當我們使用 append 要新增元素時,發現原本的容量不足,根本裝不下的話,就會重新分配「新的更大 array 」,指標也會改指向新的 array。

上述這樣的新增修改看起來很正常,不過要特別注意,因為 slice 是指向陣列,所以假設今天我們宣告了很多個 slice,並且都有修改元素的話,那被指向的那個陣列也會同時被修改唷!

來看一下範例:

// 範例
    
arr := [4]int{1, 2, 3, 4}    // 陣列 [1 2 3 4]
a := arr[0:2]                // 從陣列切出來的 slice a [1 2]
b := arr[1:3]                // 從陣列切出來的 slice b [2 3]
    
b[0] = 99                    // 改變 b[0] (其實就是 arr[1])
fmt.Println(a)               // 輸出:[1 99]
fmt.Println(arr)             // 輸出:[1 99 3 4]

從上面的這個範例就可以知道,slice 並不是 array!但是它指向 array。
所以當多個 slice 指向同一個 array 的時候,只要更改其中一個,其他的也會受到影響。

👉 常用切片用法:

  • s[:]:建立一個引用整個slice 的新slice。
  • s[low:high]:創建一個從索引 low 到 high-1 的新slice,不包含 high 索引的元素。
  • s[low:]:從 low 開始到 slice 結尾。
  • s[:high]:從開使到 high-1
  • s[:0] :清空slice,將slice 的長度設為 0。

看完以上的範例之後,大家應該會疑惑那容量 cap() 到底是怎麼計算的?

→ slice 的 容量 cap() 是從「切片起點 (index=1)」到「底層陣列結尾 (index=4)」之間,能夠使用的 最大 空間。

我們來看範例:

// 範例
    
arr := [5]int{10, 20, 30, 40, 50}
num := arr[1:4]  // 包含 arr[1], arr[2], arr[3]
len(num) = 3     // 長度 3
cap(num) = 4     // 容量 4
    
// 為什麼容量是 4?
    
arr: [10] [20] [30] [40] [50]
idx:  0    1    2    3    4
    
num := arr[1:4]   // [20] [30] [40]
len(num) = 3      // (位置 1 到 3,共 3 個元素)
cap(num) = 4      // (位置 1 到 4,最多能容納 4 個元素)

由此可知,雖然只切了 3 個元素,但是因為底層的 array 空間是 5,所以 num 雖然只切了 [20 30 40],但其實還有一個原本放 50 的空間([20 30 40 _])。

大家可以在自己的編譯器裡面用新增元素 append() 跑跑看 👇 就會知道它還有一個位子!

num = append(num, 99)   // 在 [20,30,40] 後面加一個
fmt.Println(num)        // [20 30 40 99]

所以:

  • len() = end - start (實際切出來的元素數量)。
  • cap() = len(指向底層陣列) - start(從起始位置到陣列結尾的空間)。

7. 函式 (function)

Go 的函式可以:

  1. 零個 / 一個 / 多個輸入值(包含傳入函式)
  2. 零個 / 一個 / 多個回傳值
  3. 回傳值可以自己命名
  4. 命名規則則是以英文字母小寫為使,若是兩個單字則第二個單字字母大寫,例如:add, addFunction

👉 func [函式名](輸入值名稱 輸入值型態) 輸出值型態 {}

好的,我們來看範例:

  1. 零個 / 一個 / 多個輸入值(包含傳入函式),這邊暫時不介紹傳入函式的部分。
// 範例
    
package main
import "fmt"
    
func noInput() string {                // 沒有輸入值
    return "沒有輸入值"
}
    
func input(input string) {             // 只輸入不回傳
    fmt.Println("只輸入不回傳", input)   
}
    
func add(a int, b int) int {           // 多個輸入值
    return a + b
}
    
func main() {
    	
res := noInput()
input("Hi")                  // 輸出:只輸入不回傳 Hi
sum := add(3, 5) 
    
fmt.Println(res)             // 輸出:沒有輸入值
fmt.Println(sum)             // 輸出:8
}    
  1. 零個 / 一個 / 多個回傳值
// 範例
    
package main
import "fmt"
    
func noOutput() {                      // 沒有回傳值
    fmt.Println("沒有回傳值")
}
    
func cal(a int, b int) (int, int) {    // 多個回傳值
    return a + b, a - b
}
    
func main() {
    	
    noOutput()                          // 輸出:沒有回傳值
    sum, devide := cal(3, 5) 
    
    fmt.Println(sum, devide)            // 輸出:8 -2
}

如果要將回傳值命名,可以這樣做:

// 範例
    
package main
import "fmt"
    
func cal(a int, b int) (sum int, devide int) {    // 命名回傳值 sum 和 devide
    sum = a + b
    devide = a - b
    return
}
    
func main() {
    
    sum_res, deived_res := cal(3, 5)      // 接回傳值
    fmt.Println(sum_res, deived_res)      // 輸出:8 -2
}

8. 結構體 (struct)

Go 的結構是自定義的類型,所以可以自行組合成自己想要的資料、方法。

可以把它想像成蓋房子,當今天我們想要快速地蓋好一棟房子時,會需要先把蓋房子的流程定義好,這樣開始製作的時候就可以呼叫定義好的 A、B、C 來幫助我們快速建造!

有了初步的想像之後,我們就來看範例吧!

  1. 基本宣告、存取、修改
// 範例
    
package main
import "fmt"
    
type Person struct {                       // 宣告 struct 結構
    Name string
    Age  int
}
    
func main() {
    
    p1 := Person{Name: "Alice", Age: 25}    // 建立 struct
    p2 := Person{"Bob", 30}                 // 建立的位置順序要跟宣告一致
    
    fmt.Println(p1)                         // 輸出:{Alice 25}
    fmt.Println(p2)                         // 輸出:{Bob 30}
        
        
    p3 := Person{Name: "Charlie", Age: 20}  
    fmt.Println(p3.Name)                    // 輸出:Charlie
    		
    p3.Age = 21                             // 更改 Age 為 21
    fmt.Println(p3.Age)                     // 輸出:21
}

以上就是建立 struct 的用法~ 那…如果我有兩個 struct 是有關聯的那要怎麼做呢?
那我們就直接把它放進去吧!這部分 Go 是有支援的。我們用下面的例子來看看:

// 範例
    
package main
import "fmt"
    
type Person struct {             // 宣告 struct 結構
    Name string
    Age  int
    Address                      // 直接嵌入
}
    
type Address struct {
    City string
}
    
func main() {
    data := Person{Name: "Eva", Age: 20, Address: Address{City: "Taipei"}}
    fmt.Println(data.City)      // 輸出:Taipei
    fmt.Println(data)           // 輸出:{Eva 20 {Taipei}}
}

👉 從上面的例子可以看見,data 是可以直接存取 Person 裡面 AddressCity 資料的!

這樣的機制可以做很多設計唷!是不是很方便呀~
除了可以內嵌入之外,還可以搭配 function 唷!

我們就延續上面的範例來解釋:

// 範例
    
package main
import "fmt"
    
type Person struct {                 // 宣告 struct 結構
    Name string
    Year  int
    Address                     
}
    
type Address struct {
    City string
}
    
func changeAge(a *Person) {           // 直接傳入 struct
    a.Year = a.Year + 1911
}

func (p Person)addName() string{      // 接收輸入的值,不會更改原本 struct 內容
    return p.Name + "_"
}
    
func (p *Person)changeName(s string){  // 用指標的方式,會更改值
    p.Name = s
}
    
func main() {
    data := Person{Name: "Eva", Year: 80, Address: Address{City: "Taipei"}}
    
    changeAge(&data)              // 取得 data 的位址
    fmt.Println(data.City)        // 輸出:Taipei
    fmt.Println(data)             // 輸出:{Eva 1991 {Taipei}}
    
    fmt.Println(data.addName())   // 輸出:Eva_
    	
    data.changeName("Ben")        // 輸入 Ben
    fmt.Println(data)             // 輸出:{Ben 1991 {Taipei}}
}

從上面的例子可以看到, struct 和函式搭配可以變幻出很多組合,不過對於剛接觸的人來說,也是很容易搞混的~
要特別注意的就是,有沒有 指標 會很直接的影響結果。

  1. 與 json 搭配(json tag)
    最後就是 struct 搭配 json 啦,這在 Go 很常見,因為 Go 常用來寫 API 或處理資料交換,而 JSON 是最常見的格式之一。

Go 有提供 encoding/json 的 library 來處理 struct ↔ JSON 的轉換。
所對應到的 json 資料需要用反引號 ```` 將包起來(json:"name"),我們看下面的範例:

// 範例 JSON → struct
package main
    
import (
    "encoding/json"                          // 引入 library
    "fmt"
)
    
type User struct {
    Name string `json:"name"`                // 標記對應的 JSON 欄位
    Age  int    `json:"age"`
}

func main() {
    data := `{"name": "Alice", "age": 25}`
    
    var u User
    err := json.Unmarshal([]byte(data), &u)  // 把解析的結果放進 u,要記得加上 & 才會指向原本 User的位址
    
    if err != nil {                          // 檢查解析結果,成功為 nil;失敗則印出錯誤
        fmt.Println("Error:", err)
        return
    }
    
    fmt.Println(u.Name, u.Age)               // 輸出:Alice 25
}

再來看看把 struct 轉成 JSON 的範例:

// 範例 struct → JSON
package main
    
import (
    "encoding/json"                     // 引入 library
    "fmt"
)
    
type User struct {
     Name string `json:"name"`          // 標記對應的 JSON 欄位
     Age  int    `json:"age"`
}
    
func main() {
    u := User{Name: "Bob", Age: 30}
    
    jsonData, err := json.Marshal(u)  // 轉換並回傳
    if err != nil {
    	fmt.Println("Error:", err)
    	return
    }
    		
    fmt.Println(string(jsonData))     // 輸出: {"name":"Bob","age":30}
    }

9. 指標 (pointer)

終於到最後了~
指標的用法在剛剛前面介紹的範例中也出現了一兩次,現在終於要來講它的基本用法了。
大概有這兩種用法:

  1. &:取得變數記憶體位址。
  2. *:取出(或修改)該位址存的值。

那就來看看這個簡單的基本範例吧:

// 範例
    
package main
import "fmt"
    
func main() {
    x := 10
    p := &x                             // p 存的是 x 的記憶體位址
    
    fmt.Println("x =", x)               // 輸出:x = 10
    fmt.Println("p =", p)               // 輸出:p = 0xc0000100b8(記憶體位址)
    fmt.Println("*p =", *p)             // 輸出:*p = 10(透過指標取得 x 的值)
    
    *p = 20                             // 修改指標指向的值 x
    fmt.Println("x after *p=20:", x)    // 輸出:x after *p=20: 20
}

這樣應該印出來就很清楚指標的概念了吧!
Go 的基本語法 part 2 就到這邊結束啦~


上一篇
Day3 - Go 基本語法介紹-1:變數、基本型態、條件與迴圈
下一篇
Day5 - Go 的模組化:拆分程式結構
系列文
Go,一起成為全端吧!—— 給前端工程師的 Golang 後端學習筆記5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言